- 541 - Г Л А В А 14 (Андрэ Ла Мот) С В Я З Ь --------------------- Так же, как единственная клетка мозга не могла бы сделать больших успехов в шахматах, играть с компьютером в одиночестве не очень интересно. В связи с последними достижениями в области телекоммуникаций, видеоигры для нескольких игроков становятся все более распространенными. В настоящее время многие программы поддерживают игру двух человек через модем. В этой главе мы начнем изучать системы связи между ПК и приемы, необходимые для создания видеоигр, рассчитанных на нескольких игроков. В этой главе будут изучены следующие темы: - Средства связи видеоигр; - Последовательный интерфейс ПК; - Функции поддержки последовательного порта ROM BIOS; - Соединение через нуль-модем; - Создание коммуникационных библиотек; - Стратегия коммуникационных видеоигр; - Синхронизация вектора состояния; - Синхронизация состояния порта ввода/вывода; - Синхронизация по времени; - 542 - - Модем; - Написание игры Net-Tank (Сетевой танк) для двух игроков в замкнутом пространстве. В этой главе мы сконцентрируем внимание на проблемах дизайна игр для нескольких игроков, по возможности не вдаваясь в управление коммуникациями как таковыми. К сожалению, у нас нет времени на изучение методов осуществления связи через модем в полном объеме. Однако мы научимся управлять связью через последовательный порт с помощью нуль-модема. С этими знаниями, потратив дополнительно несколько ночей, вы научитесь связываться через модем. У вас будут все элементы, достаточные для того, чтобы получить нечто работающее. Мы хотим научиться писать видеоигры для двух и более игроков, сидящих за своими компьютерами и играющими одновременно без потери синхронизации и прочих проблем. Поэтому нас больше будет интересовать тактика коммуникации в архитектуре видеоигр, а не физическая коммуникация сама по себе. СРЕДСТВА СВЯЗИ ВИДЕОИГР К этому моменту вы уже должны быть готовы писать видеоигры. Вы от начала и до конца научились тому, что для этого необходимо знать. Однако мы до сих пор упускали один важный момент: как написать игру, чтобы в нее могли играть сразу несколько человек? Это нам покуда совершенно не известно. Существуют видеоигры, поддерживающие модемное соединение, но большинство людей или не пользуются этой возможностью, или чувствуют, что она слишком сложна, чтобы к ней прибегать. В любом случае, вопрос в этой области все еще остается открытым и вам предстоит проделать немало работы, если действительно займетесь программированием игр. Поскольку нас первоначально интересует написание видеоигр для потребителей, мы будем концентрировать наши усилия на использовании серийных портов как средства коммуникации (а не сетевые коммуникации типа IPX/SPX или NETBIOS). Я выбираю параллельный порт в отличие от использования нескольких Ethernet плат по следующим причинам: - Во-первых, у каждого компьютера есть свой параллельный порт; - Во-вторых, у многих людей есть свои модемы, с помощью которых они обладают основным средством для игры вдвоем. Если мы собираемся писать видеоигры для двух или более игроков, то должны рассчитывать на это с самого начала. Вы не можете написать сложную игру и только потом вдруг решить, что делаете ее как игру для нескольких игроков. Необходимо помнить, что разработка двух независимых игр, которые запускаются и синхронно работают на двух различных ПК, требует приличных трудовых затрат и предварительного планирования. - 543 - ПОСЛЕДОВАТЕЛЬНЫЙ ИНТЕРФЕЙС ПК Последовательный порт в ПК сконструирован так, что представляет собой систему, охватывающую много различных компонентов. Нам, как программистам, не обязательно вдаваться в подробности его устройства с точки зрения аппаратуры. Однако полезно иметь представление, как он работает, даже если это никогда не понадобится. Перед тем как мы погрузимся в описание системы последовательных коммуникаций ПК, давайте рассмотрим систему тех ее возможностей, которые интересуют нас как разработчиков видеоигр. ПК могут поддерживать ло семи последовательных портов, несмотря на то, что большинство ПК имеют всего один или два последовательных порта. Мы можем сконфигурировать их для связи так, что скорость передачи может достигать 115200 бод. Мы можем также выбрать тип четности, количество стоп-битов и битов данных, а также типы прерываний, которые мы хотим задействовать. Если вы не знаете, о чем идет речь, то задержитесь здесь чуть подольше и постарайтесь понять, о чем я буду говорить. Прежде всего, нужно сконфигурировать порт, через который мы сможем общаться с другими компьютерами или устройствами, читая или записывая данные в последовательный порт. Аппаратное обеспечение ПК заботится о многих деталях передачи и приема данных. Все, что нам необходимо сделать, это послать в последовательный порт символ для передачи или обработать символ, который вы ожидаете. Для видеоигр важно, что мы посылаем пакеты информации, которые описывают состояние игры на другой машине и наоборот. Эти пакеты состоят из стандартных символов длиной 8 бит. Единственное, что нам нужно знать, это как открыть последовательный порт, записать в него символы и прочитать ответную информацию. Перейдем ближе к делу и посмотрим, что ПК могут для этого предложить (между прочим, значение некоторых терминов объяснялось чуть ранее). УНИВЕРСАЛЬНЫЙ АСИНХРОННЫЙ ПРИЕМОПЕРЕДАТЧИК ПК оборудованы универсальным асинхронным приемопередатчиком (UART) - чипом, который принимает и передает последовательные данные. Существуют два наиболее популярных UART для ПК: - Модель 8250; - Модель 16550. Можете считать, что они полностью совместимы друг с другом и нам не нужно выяснять, какой из них используется. Единственным их важным отличием является только то, что модель 16550 имеет внутренний FIFO (First In, First Out - "первый вошел - первый вышел") буфер, который располагает - 544 - входящие данные так, что они не могут потеряться вследствие задержки обработки. Теперь взглянем на кждый из регистров UART и на то, как получить к ним доступ. После того как мы обязались написать полную библиотеку для связи, необходимо уяснить, как открыть последовательный порт, а также как осуществлять чтение и запись. Написав однажды соответствующие функции, мы можем сконцентрироваться на целях игры. Установки и статус UART ------------------------------ Установки и статус UART управляются через набор внутренних регистров, доступных как порты ввода/вывода, адреса которых начинаются от некоторого базового адреса. Базовый адрес определяется номером последовательного порта, через который вы хотите связаться. Рассмотрим таблицу 14.1, в которой указаны базовые адреса управляющих регистров UART. Таблица 14.1. Базовые адреса управляющего регистра UART. --------------------------------------------------------------------- Последовательный порт Базовый адрес порта - - - - - - - - - - - - - - - - -- - -- - - - - - - ---- --- - - - - COM1 3F8h COM2 2F8h COM3 3E8h COM4 2E8h _______________________________________________________________________ Как видите, если мы хотим играть через последовательный порт COM1, нам необходимо использовать порт 3F8h в качестве базового адреса ввода/вывода. Каждый порт имеет 9 регистров, в которые можно писать или из которых можно считывать информацию в зависимости от их типа. Следовательно, для доступа к регистру 1 порта COM1 необходимо использовать адрес ввода/вывода 3F8h+1, то есть 3F9h. Теперь мы знаем, где расположены регистры. А что каждый из них делает? Регистр 0: Как регистр поддержки передачи ---------- (Transmitter-Holding Register - THR) Бит 7 6 5 4 3 2 1 0 ┌───┬── ┬───┬───┬───┬───┬───┬───┐ │ X │ X │ X │ X │ X │ X │ X │ X │ └───┴── ┴───┴───┴───┴───┴───┴───┘ Это регистр поддержки передачи, куда помещается следующий символ для передачи. Если это одиночный байт и вы используете схему передачи, имеющую менее 8 бит, то данные игнорируются и не передаются вообще. - 545 - Регистр 0: Как регистр-буфер приема (Reciever Buffer ---------- Register - RBR) Бит 7 6 5 4 3 2 1 0 ┌───┬───┬───┬───┬───┬───┬───┬───┐ │ X │ X │ X │ X │ X │ X │ X │ X │ └───┴───┴───┴───┴───┴───┴───┴───┘ Регистр 0 также выполняет функции буферного регистра приема. В зависимости от того, пишете вы в него или читаете из этого регистра, буфер передает или принимает символы с другого компьютера соответственно. В любом случае, при чтении из этого регистра он содержит последний переданный ему символ. Регистр 1: Регистр прерывания (Interrupt Enable Register - ---------- IER) Когда устанавливается этот бит, UART будет генерировать TBE: Когда установлен этот бит, прерывание всякий раз, когда UART генерирует прерывание всякий любая из линий интерфейса ──┐ раз, когда он передает RS-232 изменит свое │ символ в регистр сдвига состояние │ │ │ │ Бит 7 6 5 4 │3 2 │1 0 ┌───┬───┬───┬───┬┼──┬───┬┼──┬───┐ │ X │ X │ X │ X │ X │ X │ X │ X─┼───┐ └───┴───┴───┴───┴───┴─┼─┴───┴───┘ │ │ │ Когда установлен этот бит, │ RxRDY: Когда установлен UART будет генерировать ─────────┘ этот бит, UART генерирует прерывание по ошибке прерывание всякий раз, четности, ошибке кодирования, когда буфер приема ошибке работы и по BREAK содержит символ Этот регистр используется, чтобы задействовать тот тип прерываний, который может сгенерировать UART. Он доступен как для чтения, так и для записи. После установки серийного порта было бы неудобно постоянно опрашивать его, поэтому для получения входных данных лучше написать процедуру обслуживания прерывания (ISR), которая будет вызываться каждый раз при получении символа. Этот регистр позволяет нам сообщить UART'у, какие именно события должнывызывать прерывание. Для нас представляет интерес только прерывание RxRDY, которое генерируется при получении cимвола UART'ом. - 546 - Регистр 2: Регистр идентификации прерывания ---------- (Interrupt-Identification Register - IIR) Бит 7 6 5 4 3 2 1 0 ┌───┬───┬───┬───┬───┬───┬───┬───┐ │ X │ X │ X │ X │ X │ X │ X │ X ┼────┐ └───┴───┴───┴───┴───┴┼──┴──┼┴───┘ │ Если произошло прерывание, └──┬──┘ │ эти два бита описывают его: │ │ │ Если этот бит 0, то Бит 2 Бит 1 │ происходит прерывание, в │ противном случае - нет. 0 0 - Статус модема │ 0 1 - Пустой THR ──────────────┘ 1 0 - RBF полный 1 1 - Ошибка Регистр идентификации прерывания используется для определения причины, по которой UART выдал прерывание. Это может показаться избыточным, однако если вы предварительно установили UART для получения прерывания по двум или более различным событиям, то, поскольку этот регистр определяет тип произошедшего прерывания, он поможет вам выяснить, что именно произошло. Регистр 3: Регистр управления линией (Line-Control ---------- Register - LCR) Управляет количеством стоп-битов. Если бит 2 равен 0, Биты 1 и 0 опре- это соответствует одному стоп-биту, если он равен 1, деляют кол-во это соответствует двум стоп-битам. │ ┌────────────── посылаемых битов │ ┌──┴──┐ данных: Бит 7 6 5 4 3 │2 │1 0│ Бит 1 Бит 0 ┌───┬───┬───┬───┬───┬┼──┬┼──┬──┼┐ Это бит ──────┼ X │ X │ X │ X │ X │ X │ X │ X │ 0 0 - 5 бит ключа доступа └───┴──┼┴┼──┴─┼─┴──┼┴───┴───┴───┘ 0 1 - 6 бит к делителю │ └────┴─┬──┘ 1 0 - 7 бит (DLAB). Когда он └─────┐ └───────┐ 1 1 - 8 бит равен 1, Регистры 0 и 1 └────────┐ │ интерпретируются как делитель │ │ тактовой частоты, что определяет │ │ Биты 5, 4 и 3 управляют типом скорость передачи. При делении │ └─── четности передачи, который числа 115200 на значение, поме- │ будет послан: щенное в эти регистры, в результате │ Бит 5 Бит 4 Бит 3 получится значение скорости пере- │ 0 0 0 Отсутствует дачи. Если вы не хотите ее менять, │ 0 0 1 Нечетная этот бит должен быть равен 0. │ 0 1 1 Четная │ 1 0 1 Метка Этот бит управляет BREAK-сигналом, кото- 1 1 1 Пропуск рый будет послан в линию в момент установки бита в 1. - 547 - Регистр управления линией используется для изменения некоторых характеристик последовательного порта, таких как количество передаваемых битов данных и тип четкости. Этот регистр также выполняет функции управления загрузкой старшего и младшего байтов делителя, задающего скорость передачи и имеющего тип WORD. Этот регистр также доступен и для записи, и для чтения. Регистр 4: Регистр управления модемом (Modem-Control ---------- Register - MCR) Этот бит устанавливает Этот бит управляет модемной локальную заглушку. Если линией готовности ввода данных этот бит установлен, ввод─────┐ (DTR).DTR-линия будет отражать UART соединен с выводом. │ состояние этого бита. │ │ Бит 7 6 5 │4 3 2 1 0 │ ┌───┬───┬───┬┼──┬───┬───┬───┬───┐ │ │ X │ X │ X │ X │ X │ X │ X │ X ┼─────────┘ └───┴───┴───┴───┴─┼─┴┼──┴───┴─┼─┘ │ │ │ Этот бит управляет │ │ Этот бит управляет модемной универсальным выводом номер ───────┘ │ линией запроса на передачу 2 (GPO2) - он должен быть │ (RTS). RTS-линия будет отражать установлен в 1 для UART, │ состояние этого бита. чтобы было возможно │ посылать прерывания. │ Этот бит управляет универсальным выводом номер 1 (GPO1). Он используется для сброса заводских установок некоторых функций модема. Данный регистр оказывает влияние на некоторые выходные данные линий управления модема. Нас больше всего в нем интересует бит GPO2. Когда он установлен, появляется возможность прихода прерываний. Регистр 5: Регистр состояния линии (Line-Status Register - ---------- - LSR) TXE: Установка этого бита Этот бит устанавли- RxRDY: Установка этого означает, что UART ведет вается при возникно- бита говорит о том, передачу бит из своего вении ошибки четности. что в RBR находится внутреннего регистра сдвига. │ готовый символ. │ │ │ Бит 7 │6 5 4 3 │2 1 0 │ Если этот бит ┌───┬┼──┬───┬───┬───┬┼──┬───┬───┐ │ установлен, │ X │ X │ X │ X │ X │ X │ X │ X ┼──┘ произошла └───┴───┴──┼┴─┼─┴─┼─┴───┴─┼─┴───┘ ошибка из-за │ │ │ └───────────── несоответствия │ │ └────────────────────┐ скорости │ Если этот бит │ передачи. TBE: Если этот бит установлен,│ установлен, значит │ символ из буфера передан на ──┘ поступил BREAK-сигнал │ вход регистра сдвига. Таким (то есть RD-линия Если этот бит ус- образом, в THR может быть удерживалась в 0 более тановлен, произо- помещен следующий символ. одного такта передачи). шла ошибка коди- рования. - 548 - Регистр состояния линии используется, чтобы обрисовать остояние коммуникационного порта. В этом регистре нас интересует пятый бит (TBE), который используется для определения возможности продолжения передачи символов (THR). Регистр 6: Регистр состояния модема (Modem-Status ---------- Register - MSR) Если этот бит установлен, сиг- Если этот бит установлен, сигнал нал "свободен для пересылки" несущей частоты (CD) изменился с ──┐ (CTS) изменился с момента пос- момента последнего чтения из MSR. │ леднего чтения из регистра │ MSR. │ │ │ Бит 7 6 5 4 │3 2 1 0 │ ┌───┬───┬───┬───┬┼──┬───┬───┬───┐ │ Отражает │ X │ X │ X │ X │ X │ X │ X │ X ┼──┘ состояние линии └─┼─┴┼──┴─┼─┴─┼─┴───┴─┼─┴┼──┴───┘ данных детектора────┘ │ │ │ │ │ сигнала несущей │ │ │ │ │ Если этот бит установлен, частоты (DCD). │ │ │ │ │ сигнал готовности к приему │ │ │ │ └─ данных (DSR) изменился с Отражает состояние индика- │ │ │ момента последнего чтения тора звонка модема (RI). │ │ │ из регистра MSR. │ │ │ Отражает состояние │ │ Если этот бит установлен, состояние линии DSR модема.───────────┘ │ индикатора звонка (RI) изменилось с │ момента последнего чтения из Отражает состояние │ регистра MSR. линии CTS модема. ──────────────┘ Регистр состояния модема используется, чтобы показать состояние линий управления модемом. Для наших целей этот регистр едва ли понадобится. Однако вы могли бы найти применение индикатору звонка (RI). Вы можете написать программу, которая будет перехватывать звонок и когда вызывается ваш номер, например, сообщать об этом соответствующей надписью на экране и звуком. Регистр 7: Регистр временного заполнения (Scratch-Pad ---------- Register) Не используется Регистр 8: Менее значимый ключ делителя скорости передачи ---------- (Baud-Rate Divisor Latch Least-Significant Byte - DLL) Предназначен для хранения младшего байта делителя, используемого при вычислении действительной скорости передачи через порт. Окончательная скорость вычисляется так: берут младший и старший байты и используют их как делитель числа 115200. В результате получится скорость передачи. Этот регистр доступен через регистр 0 при установленном 7-м бите (DLAB) регистра 3 (LCR). - 549 - Регистр 9: Регистр более значимого байта ключа делителя ---------- скорости передачи (Baud-Rate Divisor Latch Most-Significant Byte - DLM) Этот регистр используется для поддержки старшего байта делителя, используемого для вычисления действительной скорости передачи через последовательный порт. Окончательная скорость передачи вычисляется следующим образом: берут старший и младший байты и используют их как делитель, на который нужно разделить число 115200. Это дает скорость передачи. Данный регистр доступен через регистр 1 при установленном 7-м бите (DLAB) регистра 3 (LCR). Аппаратное обеспечение UART ---------------------------- Разобравшись с программным обеспечением UART, давайте взглянем на его аппаратную поддержку. Нас интересуют только две вещи: куда воткнуть кабель и как сделать разъем. ПК могут иметь два типа последовательных портов: - 9-штырьковый (разъем типа DB-9); - 25-штырьковый (разъем типа DB-25). В табл.14.2 приведена их распайка. Таблица 14.2. Распайка для последовательных портов ПК. ------------------------------------------------------------------------ Провод Функция Обозначение ------------------------------------------------------------------------ 9-штырьковый разъем -------------------- 1 Сигнал наличия несущей CD 2 Прием данных RXD 3 Передача данных TXD 4 Сигнал готовности ввода данных DTR 5 Земля GND 6 Сигнал готовности набора данных DSR 7 Запрос на пересылку RTS 8 Сигнал очистки для пересылки CTS 9 Индикатор звонка RI 25-штырьковый разъем --------------------- 2 Передача данных TXD 3 Прием данных RXD __________________________________________________________________________ - 550 - Провод Функция Обозначение _________________________________________________________________________ 4 Запрос на пересылку RTS 5 Сигнал очистки для пересылки CTS 6 Сигнал готовности набора данных DSR 7 Земля GND 8 Сигнал наличия несущей CD 20 Сигнал готовности ввода данных DTR 22 Индикатор звонка RI _______________________________________________________________________ ОПЕРАЦИИ С ROM BIOS Прежде чем мы начнем писать собственную коммуникационную программу, давайте посмотрим, чем нам может помочь ROM BIOS? Она поддерживает коммуникацйии через последовательный порт, хотя и весьма ограниченно. Эти функции дотупны через прерывание 14h. Существуют функции для открытия последовательного порта, его конфигурирования, чтения и записи символа. Однако существует одна маленькая проблема: эти функции не работают (трудно в это поверить, но они действительно не работают). На самом деле они работают, правда, при особых условиях, которые нам вряд ли удастся создать. К сожалению, отсутствует крайне необходимая нам поддержка ввода/вывода, управляемого прерыванием. В видеоиграх мы должны иметь систему управления событиями, основанную на прерываниях. Из-за этого, а также из-за того, что функции ROM BIOS работают очень медленно, мы можем вообще не рассматривать их как реальное средство передачи битов по проводам. Теперь, когда я вам сказал об этом, давайте посмотрим, каким образом все-таки можно соединить два компьютера и создать маленькую сетевую игру. СОЕДИНЕНИЕ ЧЕРЕЗ НУЛЬ-МОДЕМ Как я уже сказал в самом начале, у нас не хватит времени вникать во все тонкости использования модема. Существует слишком много тем, которых мы слегка коснулись, и все они имеют отношение к дизайну видеоигр. Что я намерен сделать вместо этого, так это создать коммуникационную систему, использующую соединение типа нуль-модем, в котором модем, как таковой, отсутствует. Нуль-модем - простое соединение, которое связывает два компьютера через последовательные порты. Рис.14.1 демонстрирует такое соединение. - 551 - Рис.14.1. Соединение двух ПК. Чтобы изготовить такое соединение, мы должны взять нуль-модемный кабель с надлежащими разъемами. Это может оказаться не слишком просто, но если вы будете осторожны, то заработает с первой попытки. Мы будем использовать только три типа разъема: - Линию передачи данных; - Линию приема данных; - Землю. На рис.14.2 показано, как сделать нуль-модемный кабель для разных типов разъемов. Если вы не хотите изготавливать нуль-модемный кабель, вы можете заплатить за него в любом из компьютерных магазинов. (Пожалуйста, не платите больше 15 $. Я не могу спокойно смотреть, как люди платят за кусок провода и пластик даже 1.50$). Сейчас официальный нуль-модемный кабель использует более трех шин, которые я перечислил. Он задействует все шины, имитируя тем самым модемное соединение. Пока у нас есть TXD, RXD и земля, мы тоже при деле. (Дополнительные линии используются для аппаратного обеспечения "рукопожатия", но мы не будем ими пользоваться.) Отлично, теперь у нас есть нуль-модемный кабель, и мы знаем, какие регистры что делают в UART'е. Я думаю, что теперь самое время начать писать коммуникационное программное обеспечение. - 552 - Рис.14.2. Изготовление нуль-модемного кабеля для различных типов серийных разъемов. ПОСТРОЕНИЕ КОММУНИКАЦИОННОЙ БИБЛИОТЕКИ Нам не надо слишком много функций в нашей библиотеке. Фактически, нам достаточно шести функций: - Для инициализации последовательного порта; - Для установки процедуры обработки прерывания; - Для чтения символа из последовательного порта; - Для записи символа в последовательный порт; - Для определения состояния порта; - Для закрытия последовательного порта. Инициализация последовательного порта ------------------------------------------- Вот шаги, которым мы должны следовать: - Сначала нужно установить номера передаваемых битов, количество стоп- битов и тип четности. Это делается с помощью регистра управления линии (LCR); - 553 - - Затем устанавливается скорость передачи загрузкой старшего и младшего байта делителя; - Далее нужно инициализировать UART для управления прерываниями; - Мы должны сообщить программируемому контроллеру прерываний ПК (PIC), какие прерывания по последовательному порту он должен допускать; - Наконец, необходимо активизировать прерывание, установив бит 3 или 4 в регистре маски прерываний (соблюдая осторожность, чтобы не изменить другие его биты). Теперь обсудим эти шаги в деталях. Для инициализации порта мы должны, во-первых, установить количество бит, количество стоп-битов и тип четности. Это делается программированием регистра 3 управления линии (LCR). После этого нужно установить скорость передачи с помощью старшего и младшего байтов делителя скорости передачи. Работа с делителем немного запутана. Вы знаете, что регистры 0 и 1 выполняют дополнительные функции загрузки двухбайтового делителя, деление на который числа 115200 дает результат, используемый UART как окончательная скорость передачи. Однако, как мы знаем, регистры 0 и 1 являются соответственно регистром поддержки передачи (THR) и регистром прерывания (IER). Когда бит 7 регистра управления линией установлен в 1, они получают номера 8 и 9, но продолжают адресоваться как 1 и 0. Ясно? В качестве примера давайте установим скорость передачи в 9600 бод. Мы могли бы найти число, которое при делении на него значения 115200 давало бы 9600. Это число 12. Далее мы должны запихнуть его в младший и старший байты. В этом случае младший байт будет равен 12, а старший - 0. Далее нужно установить бит 7 (DLAB) регистра управления линией в 1 и записать младший байт в регистр 0, а старший - в регистр 1, через которые переданные байты и попадут в регистры 8 и 9. После этого необходимо очистить бит 7 (DLAB) регистра управления линии. Это было бы не так уж и плохо, а? Затем мы инициализируем UART для приема прерываний. Немного поговорим об этом. Когда UART принимает данные, символ будет оставаться в буфере приема только до тех пор, пока не прибудет следующий, после чего вновь пришедший символ заместит старый, независимо от того, был ли он считан. Мы не можем этого допустить, иначе потеряем информацию. Существует два решения этой проблемы: - Во-первых, мы могли бы в цикле опрашивать приемный буфер регистра, чтобы не потерять никаких данных. Но это было бы бесполезной тратой времени; - Во-вторых (и это гораздо лучше), можно написать для этой цели процедуру обработки прерывания (или ISR). - 554 - Если вы помните, в 12-й главе "Мнимое время, прерывания и мультизадачность", говорилось, что в Си несложно установить новое прерывание, используя ключевое слово _interrupt. Мы напишем процедуру, которая будет активизироваться всякий раз с приходом прерывания. Но как нам сообщить UART, что прерывание обнаружено? Если вы пристально посмотрите на описание регистров, то поймете, что вам необходимо установить бит здесь, бит там, и UART будет делать свою работу. Чтобы назначить прерывания, мы должны установить следующие биты в UART: - Бит 0 (RxRDY) регистра прерывания (IER) должен быть усановлен в 1; - Бит 3 (GR02) регистра управления модемом (MCR) должен быть установлен в 1. После этого мы уже готовы принимать прерывания, правильно? Ошибаетесь! Нужно сделать еще одну вещь. Мы должны сообщить программируемому контроллеру прерываний (PIC), какие именно прерывания по последовательному порту он должен задействовать. Чтобы выполнить это, необходимо изменить установки в регистре маски прерывания (IMR) PIC`а, который доступен через порт 21h. Таблица 14.3 показывает обозначение битов IMR. Таблица 14.3. Регистр маски прерывания (IMR) PIC`а. ----------------------------------------------------------------------- Бит 0: IRQ0 - используется для таймера Бит 1: IRQ1 - используется для клавиатуры Бит 2: IRQ2 - зарезервирован Бит 3: IRQ3 - COM2 или COM4 Бит 4: IRQ4 - COM1 или COM3 Бит 5: IRQ5 - жесткий диск Бит 6: IRQ6 - гибкий диск Бит 7: IRQ7 - принтер _________________________________________________________________________ Таким образом, последняя вещь, которую нам необходимо сделать для обработки прерываний и запуска - активировать нужное прерывание по биту 3 или 4. Но будьте осторожны! Регистр инвертирован, так что 0 означает включен, а 1 - выключен. Осторожно! ---------------- Будьте осторожны, когда используете регистр маски прерываний. Он действительно может причинить неприятности компьютеру. Я предлагаю сначала считывать данные и маскировать их по шаблону, а потом уже писать новые данные. - 555 - Установка прерывания -------------------------- Однажды пройдя через эти фокусы, чтобы установить простое прерывание, мы можем окончательно инсталлировать наш собственный вектор ISR, зависящий от COM-порта. Запомните, что порты 3 и 4 используют те же самые прерывания, что и порты 1 и 2 соответственно. Таблица 14.4. Векторы прерывания последовательного порта. ------------------------------------------------------------------------ Вектор Номер Адресная функция ------------------------------------------------------------------------ 0x0B 0x002C-0x002F RS-232 порт 1 0x0C 0x0030-0x0033 RS-232 порт 2 ________________________________________________________________________ Все, что нам нужно сделать для установки нового ISR, это использовать функцию Си _dos_getvect(), чтобы запомнить прежнее значение вектора, и _dos_setvect(), чтобы инсталлировать наш собственный ISR на место старого. Далее, с приходом прерывания (то есть когда получен символ), будет вызываться наша процедура. Звучит это великолепно, но что она будет делать? Наш ISR должен выполнять только одну задачу - получить символ из регистра приемного буфера (RBR) и поместить его в программный буфер. Чтобы основная программа могла брать поступающие символы по мере надобности, мы Рис.14.3. Буфер с перезаписью. - 556 - должны буферизировать ввод. С этой мыслью создадим буфер с перезаписью и установим его размер равным 128 байтам, хотя вообще-то, его длина может быть любой. Алгоритм буферизации работает так. Полученный из RBR следующий символ помещается в буфер в текущую позицию. Далее текущий индекс буфера инкрементируется. Когда позиция записи в буфере доходит до конца, она перемещается к началу. Как вы понимаете, при этом данные, которые были записаны ранее, окажутся перекрыты. Надеюсь, что до того, как это произойдет, основная программа успеет прочитать символы из буфера и обработать полученные данные. Рис.14.3 поясняет принцип работы буфера с перезаписью. Мы должны обсудить еще одну тонкость, прежде чем закончим разговор об ISR. Непосредственно перед выходом из процедуры обработки прерывания необходимо сообщить PIC`у о ее завершении. Для этого в конец процедуры нужно вставить команду записи в порт 20h значения 20h. Если этого не сделать, произойдет сбой системы. Но это - между прочим, ибо пока вы используете функции Си, об этом не стоит беспокоиться. Вот если бы вы решили писать программы исключительно на ассемблере, то вопрос правильного завершения прерываний оказался бы весьма актуален и мы обсудили бы его более подробно. Но давайте пока остановимся на Си. Листинг 14.1 показывает операции с ISR. Листинг 14.1. Операция ISR. -------------------------------------------------------------------------- void _interrupt _far Serial_Isr(void) { // Это процедура обработки прерывания COM-порта. Она очень проста. // При вызове она читает полученный символ из регистра 0 порта // и помещает его в буфер программы. Примечание: язык Си сам // заботится о сохранении регистров и восстановлении состояния // запрещаем работу всех других функций // во избежание изменения буфера serial_lock = 1; // записываем символ в следующую позицию буфера ser_ch = _inp(open_port + SER_RBF); // устанавливаем новую текущую позицию буфера if (++ser_end > SERIAL_BUFF_SIZE-1) ser_end = 0; // помещаем символ в буфер ser_buffer[ser_end] = ser_ch; ++char_ready; - 557 - // восстанавливаем состояние контроллера прерываний _outp(PIC_ICR,0x20); // разрешаем работу с буфером serial_lock = 0; } // конец функции _____________________________________________________________________ Программа из Листинга 14.1 выполняет все то, о чем мы говорили. Однако стоит обратить внимание на одну маленькую деталь. В программу включена переменная serial_lock, которая оберегает основную программу от конфликтов, связанных с обращением к буферу, пока ISR обновляет его. Такой прием называется "блокировкой" или "семафором". В DOS`е подобной проблемы никогда не возникает по ряду причин, о которых говорить слишком долго. Необходимость регулирования доступа к общим данным возникает только для полностью многозадачных систем. Тем не менее, введение "семафоров" - хорошая практика, даже если на данном этапе такая техника и не нужна. Все, мы почти у цели! Чтение символа из буфера ------------------------------ Теперь нам необходимо иметь возможность считывать символ из буфера. Это легко. В принципе, достаточно запомнить новый индекс, указывающий на текущую ячейку буфера, из которой будет прочитан следующий символ. Но что если мы попытаемся прочитать символ и изменить индекс, в то время как основная программа уже исчерпала все входные данные, пришедшие от прерывания? В этом случае функция просто-напросто будет возвращать символ 0. Листинг 14.2 содержит подходящую программу. Листинг 14.2. Функция Serial_Read. ----------------------------------------------------------------------- int Serial_Read() { // функция возвращает последний записанный // в программный буфер символ int ch; // ждем завершения функции обработки прерывания while(serial_lock){} // проверяем, есть ли символы в буфере if (ser_end != ser_start) - 558 - { // меняем значение начальной позиции буфера if (++ser_start > SERIAL_BUFF_SIZE-1) ser_start = 0; // читаем символ ch = ser_buffer[ser_start]; // в буфере стало одним символом меньше if (char_ready > 0) --char_ready; //возвращаем символ вызвавшей функции return(ch); } // коней действий, если буфер не пуст else // буфер был пуст - возвращаем 0 return(0); } // конец функции _________________________________________________________________________ Функция serial_read получает следующий доступный символ из буфера и возвращает его. Если в буфере не осталось данных, она возвращает 0. Запись в последовательный порт ---------------------------------- Нам осталось сделать функцию, которая могла бы записывать символы в последовательный порт. Для выполнения этого достаточно записать данные в регистр поддержки передачи. Однако мы должны сделать так, чтобы одновременно выполнялись два условия: - Во-первых, ISR не может быть активен; - Во-вторых, регистр поддержки передачи должен быть пуст. Первое условие выполняется как само собой разумеющееся. Однако второе должно быть проверено с помощью пятого бита регистра состояния линии. Если этот бит установлен, передатчик пуст и мы можем послать в порт следующий символ. Листинг 14.3 содержит программу для передачи символа. - 559 - Листинг 14.3. Функция Serial_Write. ------------------------------------------------------------------------ Serial_Write(char ch) { // эта функция записывает символ в буфер последовательного порта, // но вначале она ожидает, пока он освободится // Примечание: эта функция не связана с прерываниями // и запрещает их на время работы // ждем освобождения буфера while(!(_inp(open_port + SER_LSR) & 0x20)){} // запрещаем прерывания _asm cli // записываем символ в порт _outp(open_port + SER_THR, ch); // разрешаем прерывания снова _asm sti } // конец функции _________________________________________________________________________ Обратите внимание на одну хитрость, примененную в функции Serial_Write: она запрещает прерывания перед записью символа в порт и затем вновь разрешает их. КОММУНИКАЦИОННАЯ ПРОГРАММА: NLINK Программа NLINK завершает наш извилистый путь освоения последовательных коммуникаций для ПК. Я написал эту небольшую коммуникационную программку, чтобы вы могли лучше оценить пройденное. Она соединяет два ПК через COM1 или COM2 и позволяет двум игрокам общаться по нуль-модемному кабелю. Для выхода из программы надо нажать клавишу Esc. Листинг 14.4 содержит законченную коммуникационную библиотеку и главную часть программы NLINK. Листинг 14.4. Коммуникационная программа NLINK (NLINK.C). ----------------------------------------------------------------------- // I N C L U D E S /////////////////////////////////////////////////////////// #include #include #include #include #include #include // D E F I N E S //////////////////////////////////////////////////////////// // registers in UART #define SER_RBF 0 // the read buffer #define SER_THR 0 // the write buffer #define SER_IER 1 // the int. enable register #define SER_IIR 2 // the int. identification register #define SER_LCR 3 // control data config. and divisor latch #define SER_MCR 4 // modem control reg. #define SER_LSR 5 // line status reg. #define SER_MSR 6 // modem status of cts, ring etc. #define SER_DLL 0 // the low byte of baud rate divisor #define SER_DLH 1 // the hi byte of divisor latch // bit patterns for control registers #define SER_BAUD_1200 96 // baud rate divisors for 1200 baud - 19200 #define SER_BAUD_2400 48 #define SER_BAUD_9600 12 #define SER_BAUD_19200 6 #define SER_GP02 8 // enable interrupt #define COM_1 0x3F8 // base port address of port 0 #define COM_2 0x2F8 // base port address of port 1 #define SER_STOP_1 0 // 1 stop bit per character #define SER_STOP_2 4 // 2 stop bits per character #define SER_BITS_5 0 // send 5 bit characters #define SER_BITS_6 1 // send 6 bit characters #define SER_BITS_7 2 // send 7 bit characters #define SER_BITS_8 3 // send 8 bit characters #define SER_PARITY_NONE 0 // no parity #define SER_PARITY_ODD 8 // odd parity #define SER_PARITY_EVEN 24 // even parity #define SER_DIV_LATCH_ON 128 // used to turn reg 0,1 into divisor latch #define PIC_IMR 0x21 // pic's interrupt mask reg. #define PIC_ICR 0x20 // pic's interupt control reg. #define INT_SER_PORT_0 0x0C // port 0 interrupt com 1 & 3 #define INT_SER_PORT_1 0x0B // port 0 interrupt com 2 & 4 #define SERIAL_BUFF_SIZE 128 // current size of circulating receive buffer // G L O B A L S ///////////////////////////////////////////////////////////// void (_interrupt _far *Old_Isr)(); // holds old com port interrupt handler char ser_buffer[SERIAL_BUFF_SIZE]; // the receive buffer int ser_end = -1,ser_start=-1; // indexes into receive buffer int ser_ch, char_ready=0; // current character and ready flag int old_int_mask; // the old interrupt mask on the PIC int open_port; // the currently open port int serial_lock = 0; // serial ISR semaphore so the buffer // isn't altered will it is being written // to by the ISR ////////////////////////////////////////////////////////////////////////////// void _interrupt _far Serial_Isr(void) { // this is the ISR (Interrupt Service Routine) for the com port. It is very // simple. When it gets called, it gets the next character out of the receive // buffer register 0 and places it into the software buffer. Note: C takes care // of all the register saving and house work. Cool huh! // lock out any other functions so the buffer doesn't get corrupted serial_lock = 1; // place character into next position in buffer ser_ch = _inp(open_port + SER_RBF); // wrap buffer index around if (++ser_end > SERIAL_BUFF_SIZE-1) ser_end = 0; // move character into buffer ser_buffer[ser_end] = ser_ch; ++char_ready; // restore PIC _outp(PIC_ICR,0x20); // undo lock serial_lock = 0; } // end Serial_Isr ////////////////////////////////////////////////////////////////////////////// int Ready_Serial() { // this functions returns true if there are any characters waiting and 0 if // the buffer is empty return(char_ready); } // end Ready_Serial ////////////////////////////////////////////////////////////////////////////// int Serial_Read() { // this function reads a character from the circulating buffer and returns it // to the caller int ch; // wait for isr to end while(serial_lock){} // test if there is a character(s) ready in buffer if (ser_end != ser_start) { // wrap buffer index if needed if (++ser_start > SERIAL_BUFF_SIZE-1) ser_start = 0; // get the character out of buffer ch = ser_buffer[ser_start]; // one less character in buffer now if (char_ready > 0) --char_ready; // send data back to caller return(ch); } // end if a character is in buffer else // buffer was empty return a NULL i.e. 0 return(0); } // end Serial_read ////////////////////////////////////////////////////////////////////////////// Serial_Write(char ch) { // this function writes a character to the transmit buffer, but first it // waits for the transmit buffer to be empty. note: it is not interrupt // driven and it turns of interrupts while it's working // wait for transmit buffer to be empty while(!(_inp(open_port + SER_LSR) & 0x20)){} // turn off interrupts for a bit _asm cli // send the character _outp(open_port + SER_THR, ch); // turn interrupts back on _asm sti } // end Serial_Write ////////////////////////////////////////////////////////////////////////////// Open_Serial(int port_base, int baud, int configuration) { // this function will open up the serial port, set it's configuration, turn // on all the little flags and bits to make interrupts happen and load the // ISR // save the port for other functions open_port = port_base; // first set the baud rate // turn on divisor latch registers _outp(port_base + SER_LCR, SER_DIV_LATCH_ON); // send low and high bytes to divsor latches _outp(port_base + SER_DLL, baud); _outp(port_base + SER_DLH, 0); // set the configuration for the port _outp(port_base + SER_LCR, configuration); // enable the interrupts _outp(port_base + SER_MCR, SER_GP02); _outp(port_base + SER_IER, 1); // hold off on enabling PIC until we have the ISR installed if (port_base == COM_1) { Old_Isr = _dos_getvect(INT_SER_PORT_0); _dos_setvect(INT_SER_PORT_0, Serial_Isr); printf("\nOpening Communications Channel Com Port #1...\n"); } else { Old_Isr = _dos_getvect(INT_SER_PORT_1); _dos_setvect(INT_SER_PORT_1, Serial_Isr); printf("\nOpening Communications Channel Com Port #2...\n"); } // enable interrupt on PIC old_int_mask = _inp(PIC_IMR); _outp(PIC_IMR, (port_base==COM_1) ? (old_int_mask & 0xEF) : (old_int_mask & 0xF7 )); } // Open_Serial ////////////////////////////////////////////////////////////////////////////// Close_Serial(int port_base) { // this function closes the port which entails turning off interrupts and // restoring the old interrupt vector // disable the interrupts _outp(port_base + SER_MCR, 0); _outp(port_base + SER_IER, 0); _outp(PIC_IMR, old_int_mask ); // reset old isr handler if (port_base == COM_1) { _dos_setvect(INT_SER_PORT_0, Old_Isr); printf("\nClosing Communications Channel Com Port #1.\n"); } else { _dos_setvect(INT_SER_PORT_1, Old_Isr); printf("\nClosing Communications Channel Com Port #2.\n"); } } // end Close_Serial ////////////////////////////////////////////////////////////////////////////// main() { char ch; int done=0; printf("\nNull Modem Terminal Communications Program.\n\n"); // open com 1 Open_Serial(COM_1,SER_BAUD_9600,SER_PARITY_NONE | SER_BITS_8 | SER_STOP_1); // main loop while(!done) { // try and get a character from local machine if (kbhit()) { // get the character from keyboard ch = getch(); printf("%c",ch); // send the character to other machine Serial_Write(ch); // has user pressed ESC ? if so bail. if (ch==27) done=1; // test for CR, if so add an line feed if (ch==13) { printf("\n"); Serial_Write(10); } } // end if kbhit // try and get a character from remote if (ch = Serial_Read()) printf("%c", ch); if (ch == 27) { printf("\nRemote Machine Closing Connection."); done=1; } // end if remote close } // end while // close the connection and blaze Close_Serial(COM_1); } // end main __________________________________________________________________________ Изучение принципов коммуникации через последовательный порт ввода/вывода похоже на посещение зубного врача - никому не нравится, но всем приходится через это пройти. Мне жаль, что я подвергаю вас подобной пытке, но это исключительно важно знать. Посему не буду вас дольше истязать и перейду к более интересной теме игровых коммуникаций. СТРАТЕГИЯ ИГРОВЫХ КОММУНИКАЦИЙ Соединение двух ПК в запуск на них сетевой игры является комплексной задачей, не имеющей какого-то общего решения. Все зависит от конкретной цели, которую вы перед собой поставите и решение проблемы, скорее всего, будет меняться от игры к игре. Однако, существует несколько правил, которые всегда нужно принимать во внимание. Именно об этом мы и поговорим в ближайшее время. В игре для двух участников, которая запускается на одном компьютере, оба игрока имеют равные шансы влиять на игровую ситуацию. Рис.14.4 показывает два различных представления этой разновидности игр. Однако, если такая игра запускается на разных машинах, взаимоотношения игроков с внутренним миром компьютеров представляется более сложным, и это отражено на рис.14.5. - 567 - Рис.14.4. Различные представления игры для двоих. Рис.14.5. Игра для двоих на разных ПК. - 568 - Проблемы, возникающие при такой конфигурации, в основном связаны с отсутствием в непосредственной близости другого игрока (как-нибудь мы осветим тему дистанционного управления состоянием компьютера). Кроме того, компьютер должен получить достаточно информации, чтобы он смог показать действия игрока за другим компьютером. Для преодоления этих проблем существуют следующие пути: - Можно передать на другой компьютер полную информацию о действиях игрока. В этом случае коммуникационный порт будет напоминать виртуальное устройство ввода данных, управляемое другим компьютером. Когда игрок тронет клавиши, переместит мышь или повернет ручку джойстика, это действие тут же передается по кабелю другой машине, которая на основе полученных данных может скорректировать виртуальное местоположение игрока в собственном игровом пространстве; - Второй метод называется "синхронизацией вектора состояния". В этом случае вместо передачи данных от устройства ввода/вывода, мы передаем "состояние" игрового пространства в целом, так что принимающий компьютер может синхронизироваться с передающим, как показано на рис.14.6. Этот метод работает достаточно хорошо, однако при его использовании может значительно увеличиться количество передаваемой информации. Рис.14.6. Синхронизация вектора состояния. Вскоре мы разберемся с каждым из методов более детально, а сейчас стоит поговорить о наиболее типичных ошибках, встречающихся при соединении двух ПК: - Наибольшая проблема возникает, когда две машины теряют синхронизацию. Скажем, одна из них имеет 586-й процессор, а другая - 386-й. При этом один ПК неизбежно окажется впереди другого и синхронизация будет потеряна. Этот фактор должен быть принят во внимание еще на этапе разработки игры; - 569 - - Следующая потенциальная проблема может быть вызвани так называемым "недетерминированным эффектом наложения" (я расскажу лишь о некоторых лежащих на поверхности вещах, однако этого достаточно для понимания сути проблемы). Обе игры должны быть полностью детерминированы. Это значит, например, что мины на разных компьютерах не могут оказаться в различных местах. Если на одной машине мина расположена, скажем, в центре игрового поля, то и на другой машине ей лучше бы оказаться в том же месте. Точно так же, при использовании генератора случайных чисел для управления поведением существ, необходимо, чтобы на обеих машинах генерировалась одна и та же последовательность случайных величин. Единственным путем преодоления этой проблемы может служить передача полной информации об игровой ситуации, так чтобы даже случайные события, происходящие на одной машине, без искажений отражались на другой. Эти проблемы действительно очень серьезны и вам необходимо их тщательно проработать. Мы кратко обсудили основные методы синхронизации и теперь уже можно поговорить о них более подробно. СИНХРОНИЗАЦИЯ ВЕКТОРА СОСТОЯНИЯ Реализовать синхронизацию вектора состояния несложно. Для этого достаточно непрерывно передавать другой машине данные о состоянии игрового пространства и принимать ответную информацию, чтобы скорректировать обстановку. Давайте в качестве примера рассмотрим некоторую игру, в которой двое участников на разных компьютерах ведут дуэль с астероидами. Чтобы передать состояние одной машины на другую, мы должны учесть и местоположение, и скорость, и размер каждого астероида, а также не забыть передать и координаты самого игрока. Если игрок открыл огонь, мы должны передать соответствующее сообщение и об этом, а также описать атрибуты оружия. Кроме того, если в игровом пространстве на одной из машин появился новый объект, мы должны сообщить об этом друглму компьютеру, чтобы и он создал аналогичный объект. Таким образом, мы как бы делаем фотографии игрового пространства и постоянно передаем их на другую машину. Это нужно делать в разумном темпе, так, чтобы избежать всевозможных несогласованных ситуаций. Например, может же случиться так, что один игрок взрывает астероид в то время, как другой на него только нацелился. Как вы понимаете, в этом случае нужно удалить астероид из игрового пространства прежде, чем снаряд второго игрока достигнет уже не существующей цели. На рис.14.7 с некоторым преувеличением показано, что может произойти, если система выйдет из состояния синхронизации. - 570 - Рис.14.7. Что происходит, когда два ПК выходят из режима синхронизации. Синхронизация вектора состояния работает прекрасно и совершенно надежно, потому что абсолютно все происходящее на одной машине передается на другую. Однако, как я подозреваю, это не так легко реализовать: ведь во внимание принимаются все возможные характеристики состояния игры, и в результате итоговая информация, передаваемая через коммуникационный канал, оказывается достаточно объемистой. Следующий метод, о котором мы сейчас поговорим, более легок для понимания и называется синхронизацией состояния ввода/вывода. СИНХРОНИЗАЦИЯ СОСТОЯНИЯ ВВОДА/ВЫВОДА Синхронизация состояния ввода/вывода является методом, при котором статус устройств ввода данных передается на другой ПК в реальном времени. Все, что игрок делает на одном компьютере, принимающая система воспринимает как входные данные, которые использует для корректировки в своем игровом пространстве поведения образа отдаленного игрока. Рис.14.9 поясняет сказанное. - 571 - Рис.14.8. Передача устройств входных данных на другую машину. Этот метод четко работает до тех пор, пока обе системы остаются синхронизированными и не происходит никаких случайных изменений игровой ситуации. Если же подобное произойдет, то другая машина не сможет "узнать" об этом, потому как данный способ не предназначен для передачи такого рода информации. Если вам все же потребуется, чтобы происходили какие-то случайные события, вы должны воспользоваться первым методом, чтобы сообщить об изменениях другой машине. В дальнейшем мы объединим оба способа синхронизации вместе - это совершенно неизбежно. Для синхронизации состояния ввода/вывода необходимо: - Опросить текущее состояние устройств ввода данных, будь то джойстик или клавиатура; - Объединить их вместе в пакет и послать через коммуникационный канал. Термин "пакет" подразумевает объединение разносортной информации. Поэтому для пересылки пакетов мы должны принять ряд соглашений, чтобы последовательные коммуникационные системы "знали", что означает та или иная часть информации. Скажем, мы решили передать через коммуникационный канал положение ручки джойстика одновременно с состоянием его кнопок. Формат пакета для передачи этих данных мог бы выглядеть примерно так, как это показано в табл.14.5. - 572 - Таблица 14.5. Образец пакета информационного пространства ввода/вывода. ------------------------------------------------------------------------- N байта Обозначение Смысл ------------------------------------------------------------------------- 0 J Установленное состояние джойстика 1 data_x Байт Х-координаты джойстика 2 data_y Байт Y-координаты джойстика 3 buttons Байт состояния кнопок 4 (period) Конец передачи -------------------------------------------------------------------------- Составить такой пакет чрезвычайно просто. Не сложно и заставить программу передавать его на другую машину с частотой, необходимой для сохранения синхронизации. В то же самое время, дпугая машина могла бы предоставить в распоряжение первой собственный пакет состояния джойстика. Эдесь есть один не вполне очевидный нюанс. Дело в том, что оба компьютера в равной мере думают друг о друге как об удаленном игроке. (Есть в этом что-то от фантастических романов, описывающих путешествия в параллельные миры и во времени, - частенько путаешься и начинает болеть голова.) Это все, что я хотел сказать по поводу синхронизации состояния ввода/вывода. Мы еще продолжим разговор о сохранении режима синхронизации, но сейчас нам нужно обсудить такую малоизученную область, как временнАя синхронизация. ' ВРЕМЕННАЯ СИНХРОНИЗАЦИЯ Временная синхронизация означает привязку обеих машин к некоему общему таймеру, который может быть как внутренним, так и внешним. Как я уже говорил, необходимо, чтобы обе игры протекали в одном и том же темпе и каждое событие происходило на обоих компьютерах одновременно. Существует много путей для этого, но здесь приводятся только два из них: - Один путь состоит в обмене данными между компьютерами с определенным интервалом времени, который выбирается одинаковым для обеих машин. К примеру, машины производят обмен каждые 1/30 секунды. В результате система будет терять синхронизацию не более чем на 1/30 секунды. - Другой технический прием основан на ожидании посылающим компьютером подтверждения того, что сообщение принимающим ПК получено. Это показано на рис.14.9. Первый компьютер передает свое состояние другому. Как только вторая машина подтвердит получение этого сообщения и передаст свое состояние, ПК1 - 573 - Рис.14.9. Передача лексемы в состоянии синхронизации. может продолжать работу. И запомните, что с точки зрения второго ПК все должно происходить наоборот. То есть после соединения компьютеров один из них получит сообщение первым и тут же вернет его назад - сообщение начнет ходить взад и вперед. Этот метод имеет интересный побочный эффект: для сохранения состояния синхронизации компьютеры вынуждены работать с более низкой скоростью. Это происходит оттого, что максимальная задержка определяется не быстродействием ПК, а тем, как долго каждой из машин обрабатываются сообщения. Прекрасно, теперь вы просто эксперты по коммуникациям. Это было не так уж и сложно, не правда ли? Даже если вы и не стали специалистом, то по крайней мере, должны неплохо разбираться в этом вопросе и у вас появилось несколько технических приемов для решения возможных проблем. Перед тем, как мы приступим к игре Net-Tank, я хочу сказать пару слов о модеме. МОДЕМ Соединение через модем не так сложно, но описание работы с модемом добавило бы еще немало страниц, на которые у нас просто нет времени. Модем управляется по тем же принципам, что и последовательный порт, с которым он соединен. Единственное отличие состоит в том, что модем прослушивает последовательный порт, и если слышит специальную последовательность символов, то думает, что вы разговариваете с ним, и будет интерпретировать команды и выполнять их. Эти команды называются набором АТ-команд. Они поддерживают все функциональные возможности, которые вам нужны для дозвона, ответа и осуществления телефонного соединения. Как только соединение произошло, это стало прозрачно для вас. Вы можете забыть, что соединились через модем. Ну ладно, теперь пора разъединиться и перейти к игре. NET-TANK: ТАНКОВЫЙ ИМИТАТОР ДЛЯ ДВУХ ИГРОКОВ Net-Tank - игра для одного или двух игроков. Я написал ее, чтобы показать, как создаются игры для нескольких участников и осветить некоторые проблемы, которые могут при этом возникнуть. Это самая примитивная игра (для ее написания мне понадобилось всего три дня), имеющая один-единственный уровень. Более того, это двухмерная игра, в которой игрок смотрит на поле боя сверху вниз. Мы напишем полноценную трехмерную игру типа Wolfenstein в 19-й главе, а сейчас я хочу оставить программу достаточно простой, чтобы акцентировать внимание не на алгоритмах игровой логики, а на коммутационной части. Перед тем, как мы начнем разбирать игру, я предлагаю вам сыграть в нее с кем-нибудь или, по крайней мере, в одиночку, чтобы вы понимали, о чем сейчас пойдет речь. Как я упоминал ранее, все профессионалы в области разработки игр стараются писать уникальные программы и не повторять уже существующие игры. Я модифицировал некоторые модули наших старых программ и создал на их основе новые модули. Чтобы избавиться от массивных включений исходных файлов, не имеющих непосредственного отношения к игре, я разработал несколько библиотек: SNDLIB.C библиотека звуковых эффектов; GRAPF1.C низкоуровневый двухмерный спрайт и графическая библиотека; KEYLIB.C библиотека ввода с клавиатуры; SERLIB.C коммуникационная библиотека. Все файлы заголовков для этих библиотек имеют те же самые имена: SNDLIB.H GRAPH1.H KEYLIB.H SERLIB.H Все эти модули объединяются в одну обширную библиотеку с помощью менеджера LIB.EXE. Для создания этой библиотеки вам надо: (Описание процесса компиляции рассчитано на транслятор фирмы Microsoft. Если вы пользуетесь значительно более распространенным у нас транслятором фирмы Borland Int., ключи компиляции надо изменить. Программа-библиотекарь компилятора фирмы Borland Int. называется TLIB.EXE и работа с ней выглядит несколько иначе (прим.ред.)) 1. Скомпилировать каждый Си-модуль с помощью файла CO.BAT: cl -AM -Zi -c -Fc -Gs -G2 %1.c - 575 - 2. После компиляции создается одна большая библиотека с именем MYLIB.LIB. Чтобы сделать это, вызовите библиотечный менеджер путем ввода команды lib mylib.lib 3. Программа скажет, что библиотека стаким именем отсутствует, и спросит вашего согласия на ее создание. Ответьте утвердительно; 4. Добавьте в командную строку все модули, входящие в библиотеку, ка это указано: operations: +SNDLIB +GRAPH1 +KEYLIB +SERLIB 5. Затем появятся еще два запроса. Ответьте на них нажатием клавиши Enter. После этого у вас появится библиотека MYLIB.LIB, которую вы можете присоединять так же, как и любую другую библиотеку. Для создания игры вам необходимо создать два исполняемых модуля: - Один для игрока 1; - Другой для игрока 2. Чтобы это сделать, вам надо: 1. Откомпилировать NET1.C и NET2.C (2 версии игры) следующим с помощью командного файла CC.BAT: cl -AM -Zi -c -Fc -Gs -G2 %1.c if errorlevel l goto c_fail link /ST:16384 /CO %1,,,graphics.lib+mylib.lib,, :c_fail Этот командный файл компилирует игру и объединяет ее с библиотекой, чтобы создать исполняемый модуль. Сделайте это, чтобы создать два файла: NET1.EXE и NET2.EXE; 2. Поместите NET1.EXE на ПК1 и NET2.EXE на ПК2. Соедините обе машины нуль-модемным кабелем через COM1; 3. Теперь можно начинать игру. Наберите NET1.EXE на ПК1 и NET2.EXE на ПК2. Машины соединятся и вы можете сразиться со своим приятелем на танковой дуэли. Используйте следующие управляющие клавиши: Стрелка вправо Повернуть направо Стрелка влево Повернуть налево Стрелка вверх Двигаться вперед Стрелка вниз Двигаться назад - 576 - ESC Выход из игры Пробел Стрельба Т Подразнить партнера Поиграйте в Net-Tank и попутно обратите внимание на следующие вещи: - Возникает ли эффект запаздывания при перемещениях? - Теряет ли игра синхронизацию? - Если да, то в какие моменты? Также обратите внимание на звуковые эффекты. Для их создания я использовал собственный голос и условно-бесплатную программу Blaster Master. Наконец, игру можно проводить при установленной программе-ускорителе клавиатуры. Если при нажатии клавиши танк слишком резво устремляется вперед, попробуйте уменьшить скорость реагирования клавиатуры, установив программу TURBOKEY.COM, которая имеется на дискете. АНАЛИЗ ИГРЫ NET-TANK Если вы обзовете Net-Tank пережитком каменного века, я полностью соглашусь с вами. Однако она содержит несколько интересных технических приемов, которые вы можете использовать (и которые в дальнейшем будут применены в Warlock`е). Вся игровая логика содержится в функции main() Си-программы. Я сделал это для того, чтобы легче было обозреть игру в целом. Исключение составляют только вызываемые функции, которые являются низкоуровневыми, но обычно их имена говорят о том, для чего они предназначены или что они делают (например, сложно не понять, что означает Draw_Sprite). Основная часть включает в себя пару сотен строк, и если вы поймете их смысл, вы в хорошей форме. Давайте разберем игру, рассматривая раздел за разделом. Раздел 1: Инициализация ----------------------- В этой части программы мы загружаем все файлы со звуковыми эффектами и графикой для игры. Элементы изображения для танков берутся из загруженных файлов и размещаются в предварительно выделенной под буфер области памяти. Net-Tank использует технику дублирующей буферизации для исключения мерцания излбражения. Напомню, это означает, что изображение вначале формируется в оперативной памяти и в уже полностью подготовленном виде копируется в видеопамять. Кроме того, во время инициализации структуры данных, описывающие все игровые объекты, обнуляются и устанавливаются в исходное положение. Уже при инициализации возникает первый вопрос сетевой игры. Программы, работающие на обеих машинах, почти идентичны, но должна быть небольшая - 577 - разница: ваш компьютер должен показать объект противника в том же месте, где он изначально расположен на другом ПК и наоборот. Это означает, что исходные позиции игровых объектов должны быть жестко установлены. Этого можно добиться, задав позиции в качестве констант в тексте программы, загружая их из неизменяемого файла данных или задавая при старте программы по определенному алгоритму. Способ, с помощью которого это делается, не важен. Однако при старте программы игрок, который сточки зрения одной машины является местным, будет удаленным с точки зрения другого компьютера, и наоборот. Это может привести к путанице с координатами и проблемам с синхронизацией. Раздел 2: Игровой цикл ------------------------ Следующая часть начинается с главного цикла. Обратите внимание, что в игре имеется два цикла: внешний и внутренний. Внешний игровой цикл используется для инициализации некоторых переменных. затем начинается внутренний цикл. Именно в нем и происходит основное действие. Заметьте, что игра тазличает, когда она находится в состоянии соединения, а когда - нет. Раздел 3: Удаление объектов --------------------------- Как вы узнали из предыдущих глав, посвященных графике, прежде чем рисовать спрайты на новом месте, мы должны удалить их из прежней позиции. Эта часть программы убирает с игрового поля все движущиеся объекты путем восстановления ранее сохраненного фона под ними. Раздел 4: Получение входных данных и передача состояния дистанционно управляемой системе -------------------------------------------------------------------- Здесь начинается самое приятное. Эта часть программы подразделена на два фрагмента: - Первый из них принимает входные данные от локального игрока; - Другой принимает входные данные от удаленного игрока. Любопытно то, что оба фрагмента делают практически одно и то же. Разница только в том, что второй фрагмент обращает больше внимания на то, что поступает из последовательного порта, а первый в основном интересуется клавиатурой. Давайте остановимся и чуть-чуть поговорим о том, как происходит соединение. Как я говорил несколькими страницами раньше, для осуществления соединения применяются два основных метода: - Можно передать состояние игры в целом; - 578 - - Вы можете посылать статус устройств ввода и трактовать это как прием данных от другого джойстика или клавиатуры. В Net-Tank я применил второй метод. Один раз в течение цикла второй машине передаются все манипуляции игрока с клавиатурой. В это же время другой компьютер интерпретирует полученные по сети данные как действия со своей собственной клавиатурой. Помните, чтобы этот технический прием работал, обе игры должны быть полностью детерминированы. Никаких случайностей быть не должно. В Net-Tank я полностью следовал этому правилу всюду, кроме фрагмента, изображающего взрыв. Обычно этого не сложно избежать, но иногда, после уничтожения одного из танков, игры теряют синхронизацию. Раздел 5: Перемещение объектов ------------------------------ Следующая часть программы занимается перемещением объектов. Мы просто используем информацию, поступающую от локального и дистанционного ввода для передвижения и разворота танков. Интересен способ перемещения танков. Они могут двигаться в 16 различных направлениях, угол между которыми составляет 22,5 градуса. Вообще-то, чтобы переместить танк в выбранном направлении нам потребовалось бы, прежде всего, найти угол, а затем вычислить его синус и косинус для нахождения параметров передвижения. Однако в нашей игре синусы и косинусы вычислены заранее. Это позволяет присвоить каждому из направлений свой номер (который будет совпадать с номером текущего кадра, изображающего танк) и использовать его как индекс в таблице, содержащей значения соответствующих передвижений по осям координат. Поверьте, это лучше, чем использование математики с плавающей запятой, да еще вместе с тригонометрическими функциями. Вы узнаете гораздо больше о подобных фокусах в 18-й главе "Техника оптимизации". Во всяком случае, как только танки двинутся, сразу же можно открывать огонь. Раздел 6: Распознавание столкновений ------------------------------------- Когда для всех объектов определено их новое местоположение, нужно посмотреть, не пересекаются ли они в каких-нибудь точках, то есть, не нарушили ли они принцип исключительности Паули, полагающий что две различные частицы не могут в одно и то же время находиться в одном и том же месте. Применительно к нашей программе, мы должны ответить на вопрос: попал ли снаряд во вражеский или в наш собственный танк? Для этого координаты всех снарядов проверяются на совпадение с координатами танка. Если происходит попадание, оно фиксируется с тем, чтобы в конце цикла игры изобразить взрыв. Что касается столкновений танков друг с другом, - 579 - то пока позволим им это делать беспрепятственно (предлагаю вам доработать программу в этой части самостоятельно). Кроме этого нам нужно позаботиться о том, чтобы танки не могли проходить сквозь стены. Если вы помните, игровое поле представляет собой матрицу элементов, имеющую определенную размерность. В Net-Tank размер игрового поля составляет 20х11 ячеек, каждая из которых имеет площадь 16х16 пикселей. Следовательно, чтобы увидеть, не столкнулся ли танк со стенкой, то есть попал в занятую ячейку, необходимо: - Разделить две координаты танка на 16; - Округлить результат до целого; _ Использовать полученное значение как индекс ячейки игрового поля, чтобы увидеть, есть ли там блок. Если столкновение произошло, вернуть танк в прежнюю позицию. Раздел 7: Рисование объектов ---------------------------- Теперь мы готовы нарисовать все объекты. Чтобы сделать это, мы должны сохранить фон в тех местах, где мы планируем разместить объекты. После этого их можно нарисовать. На этом этапе игровой экран полностью построен в дублирующем буфере и теперь можно посмотреть, что же у нас получилось. Раздел 8: Дублирующий буфер --------------------------- В Net-Tank используется техника дублирующей буферизации для исключения мерцания изображения. В этой части игры крайне быстрый цикл ассемблера копирует дублирующий буфер на экран. Однако мы перемещаем только первые 176 строк из буфера, так как нижняя часть экрана неизменна и ее не надо перерисовывать. Раздел 9: Всякая всячина ------------------------ Игра прошла очередной цикл. Новая игровая ситуация отображена на экране и мы готовы вернуться к началу цикла игры. Но перед этим нам нужно сделать кое-что еще. Во время этой фазы игры перерисовывается игровое пространство, сверкают огни взрывов, а переменные, нуждающиеся в сбросе, принимают исходные значения. Раздел 10: Опять и снова опять... --------------------------------- Безусловный переход к секции 1. - 580 - ИТОГ Мы не только научились работать последовательным портом, но и создали полную библиотеку, которая может быть использована в будущем с небольшими модификациями. Мы также научились соединять в сеть два компьютера и узнали о факторах, которые надо при этом учитывать. Наконец, у нас появилась полная (вернее, почти полная) сетевая игра.